package org.osami.gateway.watteco;

import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;

import org.apache.felix.ipojo.ComponentInstance;
import org.apache.felix.ipojo.Factory;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.ServiceProperty;
import org.apache.felix.ipojo.annotations.Validate;
import org.osami.gateway.watteco.interfaces.IW1NodeIPv4;
import org.osami.gateway.watteco.interfaces.NodeInfoIPv4;
import org.osgi.service.device.Constants;

@Component
@Provides
public class W1NodeIPv4 implements IW1NodeIPv4
{
    private static final Logger logger = Logger.getLogger(W1NodeIPv4.class.getName());

    private final static String[] HEX_DIGITS = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" };

    @ServiceProperty
    private String portName;

    private SerialPort port;

    private Set<String> plugs;

    @ServiceProperty
    private String address;

    private InputStream portIn;

    private OutputStream portOut;

    private short datagramID;

    private List<ComponentInstance> componentInstances;

    @Requires(filter = "(factory.name=org.osami.gateway.watteco.WattecoSmartplugIPv4)")
    Factory nodeFactory;

    private volatile boolean quit;
    private final long delay = 1500;
    private final long dutyCycle = 8;
    private final String instanceNamePrefix = "org.osami.gateway.watteco.WattecoSmartplugIPv4_";


    public W1NodeIPv4()
    {
        super();
        this.componentInstances = new ArrayList<ComponentInstance>();
    }

    public void noDriverFound() {
        logger.info("W1NodeIPv4.noDriverFound()");
    }

    @Validate
    private void start()
    {
        logger.info("Starting W1NodeIPv4 " + address);
        init();
    }

    @Invalidate
    private void stop()
    {
        logger.info("Stopping W1NodeIPv4 " + address);
        close();
    }

    /**
     * @param port
     * @throws NoSuchPortException
     * @throws PortInUseException
     * @throws UnsupportedCommOperationException
     * @throws IOException if communication can not be set up properly
     */
    public void init()
    {
        try {
            String actualPortName;
            actualPortName = System.getenv("OSAMI_WATTECO_IPV4_PORTNAME");
            if (actualPortName == null) {
                actualPortName = portName;
            }
            CommPortIdentifier portId = CommPortIdentifier.getPortIdentifier(actualPortName);
            this.port = (SerialPort) (portId.open("W1", 1500));
            this.port.setSerialPortParams(38400, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
            this.port.enableReceiveTimeout(1000);
            this.port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
            this.plugs = new TreeSet<String>();
            this.portIn = this.port.getInputStream();
            this.portOut = this.port.getOutputStream();
            this.datagramID = 0;
        }
        catch (Exception e) { // NoSuchPortException, PortInUseException, UnsupportedCommOperationException, IOException
            throw new RuntimeException(e);
        } 
    }

    public void close()
    {
        this.port.close();
    }

    public Set<String> getPlugs()
    {
        return Collections.unmodifiableSet(this.plugs);
    }

    private short nextDatagramId()
    {
        return (short) (this.datagramID + 1 % 65536);
    }

    private static String shortToHex(short value)
    {
        String result = "";

        // 4th digit (least)
        result = HEX_DIGITS[value & 0x000F] + result;

        // 3th digit
        result = HEX_DIGITS[(value & 0x00F0) >>> 4] + result;

        // 2nd digit
        result = HEX_DIGITS[(value & 0x0F00) >>> 8] + result;

        // 1st digit
        result = HEX_DIGITS[(value & 0xF000) >>> 12] + result;

        return result;
    }

    /**
     * @param header
     * @return
     */
    private static String calculateIPChecksum(String header)
    {
        int sum = 0;

        while (true)
        {
            if (header.length() < 4)
                break;

            String currentNumber = header.substring(0, 4);
            header = header.substring(4);
            sum += Integer.parseInt(currentNumber, 16);
        }

        while (true)
        {
            sum = (sum >>> 16) + (sum & 0x0000FFFF);
            if ((sum >>> 16) == 0)
                break;
        }

        return shortToHex((short) (~sum & 0x0000FFFF));
    }

    private String getResponse(long delay)
    {
        Timer timer = new Timer(delay);
        timer.start();
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        while (true)
        {
            if (timer.hasExpired()) break;
            int incoming = 0;
            try
            {
                incoming = this.portIn.read();
            }
            catch (IOException e)
            {
                logger.severe("Exception while reading");
                break;
            }
            if (incoming == -1)
            {
                logger.fine("End of Stream");
                break;
            }
            bos.write(incoming);
        }
        String result = "";
        try
        {
            result = bos.toString("US-ASCII");
        }
        catch (UnsupportedEncodingException e)
        {
            e.printStackTrace();
        }
        return result;
    }

    private int parseResponse (String resp)
    {
        if (!resp.equals(""))
        {
            String retour = "";
            String temp = "";
            String addrSrc;
            String addrDest;
            String dataUDP = "";

            try
            {
                temp = resp.substring(25);
                addrSrc = temp.substring(0,8);
                addrDest = temp.substring(8,16);
                dataUDP = resp.substring(57);

                if (dataUDP.startsWith("01"))
                {
                    logger.info("Association OK : " + addrSrc);
                    return 0;
                }
                else if (dataUDP.startsWith("06"))
                {
                    logger.info("De-association OK : " + addrSrc);
                    return 0;
                }
                else if (dataUDP.startsWith("0C"))
                {
                    String deviceType = dataUDP.substring(2, 4);
                    String relayState = dataUDP.substring(4, 6);
                    String activePower = dataUDP.substring(6, 14);
                    String nbSec = dataUDP.substring(14, 18);
                    logger.info("Device type : "+deviceType+"\n Relay state : "+relayState+"\n Active power : "+activePower+"\n Seconds of Accumulation : "+nbSec);
                    if (activePower != null)
                    {
                        int nbSecInt = Integer.parseInt(nbSec, 16);
                        if (nbSecInt != 0) {
                            return (Integer.parseInt(activePower, 16) / nbSecInt);
                        }
                        else {
                            return 0;
                        }
                    }
                    else
                    {
                        return -1;
                    }
                }
                else
                {
                    logger.severe("ERROR: unidentified tag value.\n" + dataUDP);
                    return -1;
                }

            }
            catch (Exception e)
            {
                e.printStackTrace();
                logger.severe("ERROR: bad format.\n" + resp);
                return -1;
            }
        }
        else
        {
            logger.info("No response from module.");
            return -1;
        }
    }

    public String validateAddress(String moduleAddress)
    {
        moduleAddress = moduleAddress.toUpperCase();
        if (moduleAddress.matches("[0-9A-F]{8}"))
        {
            return moduleAddress;
        }
        else
        {
            logger.severe("Improper module address \"" + moduleAddress + "\", skipping");
            return null;
        }
    }
    
    public synchronized void lspAssocPoll() throws IOException
    {
        // Broadcast
        // Datagram is 29 bytes long
        String preHeaderIP = "4500001D" + shortToHex(nextDatagramId()) + "00004011";
        String postHeaderIP = this.address + "FFFFFFFF";

        String chkIP = calculateIPChecksum(preHeaderIP + postHeaderIP);

        String headerIP = preHeaderIP + chkIP + postHeaderIP;

        String datagramIP = "#" + headerIP + "F0B1F0B00009FFFF00" + "\n";

        logger.fine("Datagram Assoc Poll = " + datagramIP);

        this.portOut.write(datagramIP.getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(5000);
        System.out.print(response);
        parseResponse(response);
        logger.fine("Finished");
    }

    public synchronized void lspDissocReq(String moduleAddress) throws IOException
    {
        moduleAddress = validateAddress(moduleAddress);
        if (moduleAddress != null)
        {
            // Broadcast
            // Datagram is 29 bytes long
            String preHeaderIP = "4500001D" + shortToHex(nextDatagramId()) + "00004011";
            String postHeaderIP = this.address + moduleAddress;

            String chkIP = calculateIPChecksum(preHeaderIP + postHeaderIP);

            String headerIP = preHeaderIP + chkIP + postHeaderIP;

            String datagramIP = "#" + headerIP + "F0B1F0B00009FFFF05" + "\n";

            logger.fine("Datagram Dissoc Req = " + datagramIP);

            this.portOut.write(datagramIP.getBytes("US-ASCII"));
            this.portOut.flush();

            String response = this.getResponse(5000);
            System.out.print(response);
            parseResponse(response);
            logger.fine("Finished");
        }
    }

    public synchronized String getW1ID() throws IOException
    {
        logger.fine("Sending '?'");
        this.portOut.write("?".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(250);
        System.out.println(response);
        logger.fine("Finished");

        return response;
    }

    public synchronized List<NodeInfoIPv4> getNodesInfo() throws IOException
    {
        logger.fine("Sending SN");
        this.portOut.write("$SN;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(5000);
        System.out.print(response);
        logger.fine("Finished");

        // Parse response
        List<NodeInfoIPv4> result = new LinkedList<NodeInfoIPv4>();
        BufferedReader br = new BufferedReader(new StringReader(response));

        while (true)
        {
            String line = br.readLine();
            if (line == null) break;
            String[] tokens = line.split(":");
            if (tokens.length != 4) continue;
            NodeInfoIPv4.Type type = NodeInfoIPv4.Type.PLUG;
            if (tokens[3].indexOf("W1") != -1) {
                // Don't store this special node.
                type = NodeInfoIPv4.Type.W1;
            }
            if (tokens[0].indexOf("Node ") == 0) {
                result.add(new NodeInfoIPv4(tokens[2].trim(), type));
            }
        }
        return result;
    }

    public synchronized void viewHelp() throws IOException
    {
        logger.fine("Sending H");
        this.portOut.write("$H;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(1000);
        System.out.print(response);
        parseResponse(response);
        logger.fine("Finished");
    }

    public synchronized void sendRouteDiscovery() throws IOException
    {
        logger.fine("Sending Q");
        this.portOut.write("$Q;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(5000);
        System.out.print(response);
        parseResponse(response);
        logger.fine("Finished");
    }

    public synchronized void sendFullRouteDiscovery() throws IOException
    {
        logger.fine("Sending T");
        this.portOut.write("$T;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(5000);
        System.out.print(response);
        parseResponse(response);
        logger.fine("Finished");
    }

    public synchronized void sendReset() throws IOException
    {
        logger.fine("Sending RESET");
        this.portOut.write("$RESET;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(5000);
        System.out.print(response);
        parseResponse(response);
        logger.fine("Finished");
    }

    public synchronized void sendFullReset() throws IOException
    {
        logger.fine("Sending CADAR");
        this.portOut.write("$CADAR;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(5000);
        System.out.print(response);
        parseResponse(response);
        logger.fine("Finished");
    }

    public synchronized void discoverSmartplugs() throws Exception {

        this.lspAssocPoll();
        this.lspAssocPoll();
        this.lspAssocPoll();

        List<NodeInfoIPv4> nodes = this.getNodesInfo();
        for (NodeInfoIPv4 nodeInfo : nodes)
        {
            if (NodeInfoIPv4.Type.PLUG.equals(nodeInfo.getNodeType())) {
                String plugId = nodeInfo.getID();

                plugId = validateAddress(plugId);
                if (plugId != null) {
                    boolean found = false;
                    String expectedInstanceName = instanceNamePrefix + plugId;
                    
                    for (ComponentInstance instance : componentInstances) {
                        String instanceName = instance.getInstanceName();
                        if (instanceName.equals(expectedInstanceName)) {
                            logger.info("Found existing WattecoSmartplugIPv4 instance for " + plugId + ", not creating one");
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        logger.info("Creating WattecoSmartplugIPv4 instance for " + plugId + ".");

                        Dictionary<String, Object> requiresFilters = new Hashtable<String, Object>();
                        requiresFilters.put("power", "(&(sense.type=Power)(device.id=" + plugId + "))");
                        
                        Dictionary<String, Object> instanceConfiguration = new Hashtable<String, Object>();
                        instanceConfiguration.put(Constants.DEVICE_SERIAL, plugId);
                        instanceConfiguration.put("instance.name", expectedInstanceName);
                        instanceConfiguration.put("requires.filters", requiresFilters);

                        // Call factory for instance creation
                        ComponentInstance ci = nodeFactory.createComponentInstance(instanceConfiguration);
                        componentInstances.add(ci);
                        logger.info("WattecoSmartplugIPv4 instance for " + plugId + " created.");
                    }
                }
            }
        }
    }

    public synchronized void switchOn(String switchID) throws IOException
    {
        setSwitchState(switchID, true);
    }

    public synchronized void switchOff(String switchID) throws IOException
    {
        setSwitchState(switchID, false);
    }

    public synchronized void toggleSwitch(String switchID) throws IOException
    {
        throw new UnsupportedOperationException("Not implemented !");
    }

    public synchronized void setSwitchState(String moduleAddress, boolean state) throws IOException
    {
        moduleAddress = validateAddress(moduleAddress);
        if (moduleAddress != null)
        {
            // Broadcast
            // Datagram is 29 bytes long
            String preHeaderIP = "4500001E" + shortToHex(nextDatagramId()) + "00004011";
            String postHeaderIP = this.address + moduleAddress;

            String chkIP = calculateIPChecksum(preHeaderIP + postHeaderIP);

            String headerIP = preHeaderIP + chkIP + postHeaderIP;

            String etatRelay = "00";
            if (state)
            {
                etatRelay = "01";
            }

            String datagramIP = "#" + headerIP + "F0B1F0B0000AFFFF0B" + etatRelay + "\n";

            logger.fine("Datagram State Set = " + datagramIP);

            this.portOut.write(datagramIP.getBytes("US-ASCII"));
            this.portOut.flush();

            String response = this.getResponse(5000);
            System.out.print(response);
            parseResponse(response);
            logger.fine("Finished");
        }
    }

    // This command resets the measurements.
    public synchronized int getSwitchState(String moduleAddress) throws IOException
    {
        moduleAddress = validateAddress(moduleAddress);
        if (moduleAddress != null)
        {
            // Broadcast
            // Datagram is 29 bytes long
            String preHeaderIP = "4500001D" + shortToHex(nextDatagramId()) + "00004011";
            String postHeaderIP = this.address + moduleAddress;

            String chkIP = calculateIPChecksum(preHeaderIP + postHeaderIP);

            String headerIP = preHeaderIP + chkIP + postHeaderIP;

            String datagramIP = "#" + headerIP + "F0B1F0B00009FFFF0A" + "\n";

            logger.fine("Datagram State Req = " + datagramIP);

            this.portOut.write(datagramIP.getBytes("US-ASCII"));
            this.portOut.flush();

            String response = this.getResponse(5000);
            System.out.print(response);
            logger.fine("Finished");
            return parseResponse(response);
        }
        return 0;
    }

    public void startMeasurements(String meterID) throws IOException
    {
        getMeasurement(meterID);
    }

    public int getMeasurement(String meterID) throws IOException
    {
        return getSwitchState(meterID);
    }
}
